Serving Images More Effectively
Any page we get from the
Web today is topped with so many images and is so well conceived and
designed that often the overall page looks more like a magazine
advertisement than an HTML page. Looking at the current pages displayed
by portals, it’s rather hard to imagine there ever was a time—and it was
only seven or eight years ago—when one could create a Web site by using
only a text editor and some assistance from a friend who had a bit of
familiarity with Adobe PhotoShop.
In spite of the wide use
of images on the Web, there is just one way in which a Web page can
reference an image—by using the HTML <img>
tag. By design, this tag points to a URL. As a result, to be
displayable within a Web page, an image must be identifiable through a
URL and its bits should be contained in the output stream returned by
the Web server for that URL.
In many cases, the URL
points to a static resource such as a GIF or JPEG file. In this case,
the Web server takes the request upon itself and serves it without
invoking external components. However, the fact that many <img> tags on the Web are bound to a static file does not mean there’s no other way to include images in Web pages.
Where
else can you turn to get images aside from picking them up from the
server file system? For example, you can load images from a database or
you can generate or modify them on the fly just before serving the bits
to the browser.
Loading Images from Databases
The use of a database
as the storage medium for images is controversial. Some people have good
reasons to push it as a solution; others tell you bluntly they would
never do it and that you shouldn’t either. Some people can tell you
wonderful stories of how storing images in a properly equipped database
was the best experience of their professional life. With no fear that
facts could perhaps prove them wrong, other people will confess that
they would never use a database again for such a task.
The facts say
that all database management systems (DBMS) of a certain reputation and
volume have supported binary large objects (BLOB) for quite some time.
Sure, a BLOB field doesn’t necessarily contain an image—it can contain a
multimedia file or a long text file—but overall there must be a good
reason for having this BLOB support in SQL Server, Oracle, and similar
popular DBMS systems!
To read an image from a BLOB field with ADO.NET, you execute a SELECT statement on the column and use the ExecuteScalar
method to catch the result and save it in an array of bytes. Next, you
send this array down to the client through a binary write to the
response stream. Let’s write an HTTP handler to serve a database-stored
image:
public class DbImageHandler : IHttpHandler
{
public void ProcessRequest(HttpContext ctx)
{
// Ensure the URL contains an ID argument that is a number
int id = -1;
bool result = Int32.TryParse(ctx.Request.QueryString["id"], out id);
if (!result)
ctx.Response.End();
string connString = "...";
string cmdText = "SELECT photo FROM employees WHERE employeeid=@id";
// Get an array of bytes from the BLOB field
byte[] img = null;
SqlConnection conn = new SqlConnection(connString);
using (conn)
{
SqlCommand cmd = new SqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("@id", id);
conn.Open();
img = (byte[])cmd.ExecuteScalar();
conn.Close();
}
// Prepare the response for the browser
if (img != null)
{
ctx.Response.ContentType = "image/jpeg";
ctx.Response.BinaryWrite(img);
}
}
public bool IsReusable
{
get { return true; }
}
}
There are quite a few assumptions made in this code. First, we assume that the field named photo
contains image bits and that the format of the image is JPEG. Second,
we assume that images are to be retrieved from a fixed table of a given
database through a predefined connection string. Finally, we’re assuming
that the URL to invoke this handler includes a query string parameter
named id.
Notice the attempt to convert the value of the id
query parameter to an integer before proceeding. This simple check
significantly reduces the surface attack for malicious users by
verifying that what is going to be used as a numeric ID is really a
numeric ID. Especially when you’re inoculating user input into SQL query
commands, filtering out extra characters and wrong data types is a
fundamental measure for preventing attacks.
The BinaryWrite method of the HttpResponse object writes an array of bytes to the output stream.
Warning
If
the database you’re using is Northwind (as in the preceding example),
an extra step might be required to ensure that the images are correctly
managed. For some reason, the SQL Server version of the Northwind
database stores the images in the photo
column of the Employees table as OLE objects. This is probably because
of the conversion that occurred when the database was upgraded from the
Microsoft Access version. As a matter fact, the array of bytes you
receive contains a 78-byte prefix that has nothing to do with the image.
Those bytes are just the header created when the image was added as an
OLE object to the first version of Access. Although the preceding code
works like a champ with regular BLOB fields, it must undergo the
following modification to work with the photo field of the Northwind.Employees database: Response.OutputStream.Write(img, 78, img.Length);
Instead of using the BinaryWrite call, which doesn’t let you specify the starting position, use the code shown here. |
A sample page to test BLOB field access is shown in Figure 6.
The page lets users select an employee ID and post back. When the page
renders, the ID is used to complete the URL for the ASP.NET Image control.
string url = String.Format("dbimage.axd?id={0}",
DropDownList1.SelectedValue);
Image1.ImageUrl = url;
An HTTP handler must be registered in the web.config file and bound to a public endpoint. In this case, the endpoint is dbimage.axd and the script to enter in the configuration file is shown next:
<httpHandlers>
<add verb="*" path="dbimage.axd"
type="Core35.Components.DbImageHandler,Core35Lib"/>
</httpHandlers>
Note
The
preceding handler clearly has a weak point: it hard-codes a SQL command
and the related connection string. This means that you might need a
different handler for each different command or database to access. A
more realistic handler would probably use an external and configurable
database-specific provider. Such a provider can be as simple as a class
that implements an agreed interface. At a minimum, the interface will
supply a method to retrieve and return an array of bytes. Alternatively,
if you want to keep the ADO.NET code in the handler itself, the
interface will just supply members that specify the command text and
connection string. The handler will figure out its default provider from
a given entry in the web.config file. |
Serving Dynamically Generated Images
Isn’t it true
that an image is worth thousands of words? Many financial Web sites
offer charts and, more often than not, these charts are dynamically
generated on the server. Next, they are served to the browser as a
stream of bytes and travel over the classic response output stream.
But can you create and manipulate server-side images? For these tasks,
Web applications normally rely on ad hoc libraries or the graphic engine
of other applications (for example, Microsoft Office applications).
ASP.NET applications
are different and, to some extent, luckier. ASP.NET applications, in
fact, can rely on a powerful and integrated graphic engine capable of
providing an object model for image generation. This server-side system
is GDI+, and contrary to what some people might have you believe, GDI+
is fair game for generating images on the fly for ASP.NET applications.
As its name
suggests, GDI+ is the successor of GDI, the Graphics Device Interface
included with versions of the Windows operating system that shipped
before Windows XP. The .NET Framework encapsulates the key GDI+
functionalities in a handful of managed classes and makes those
functions available to Web, Windows Forms, and Web service applications.
Most of the GDI+ services
belong to the following categories: 2D vector graphics and imaging. 2D
vector graphics involve drawing simple figures such as lines, curves,
and polygons. Under the umbrella of imaging are functions to display,
manipulate, save, and convert bitmap and vector images. Finally, a third
category of functions can be identified—typography, which includes the
display of text in a variety of fonts, sizes, and styles. Having the
goal of creating images dynamically, we are most interested in drawing
figures and text and in saving the work as JPEGs or GIFs.
In ASP.NET, writing
images to disk might require some security adjustments. Normally, the
ASP.NET runtime runs under the aegis of the NETWORK SERVICE
user account. In the case of anonymous access with impersonation
disabled—which are the default settings in ASP.NET—the worker process
lends its own identity and security token to the thread that executes
the user request of creating the file. With regard to the default
scenario, an access denied exception might be thrown if NETWORK SERVICE lacks writing permissions on virtual directories—a pretty common situation.
ASP.NET and GDI+
provide an interesting alternative to writing files on disk without
changing security settings: in-memory generation of images. In other
words, the dynamically generated image is saved directly to the output
stream in the needed image format or in a memory stream.
Writing Copyright Notes on Images
GDI+ supports quite a few image formats, including JPEG, GIF, BMP, and PNG. The whole collection of image formats is in the ImageFormat structure from the System.Drawing namespace. You can save a memory-resident Bitmap object to any of the supported formats by using one of the overloads of the Save method:
Bitmap bmp = new Bitmap(file);
...
bmp.Save(outputStream, ImageFormat.Gif);
When you attempt to save
an image to a stream or disk file, the system attempts to locate an
encoder for the requested format. The encoder is a GDI+ module that
converts from the native format to the specified format. Note that the
encoder is a piece of unmanaged code that lives in the underlying Win32
platform. For each save format, the Save method looks up the right encoder and proceeds.
The next example wraps
up all the points we touched on. This example shows how to load an
existing image, add some copyright notes, and serve the modified version
to the user. In doing so, we’ll load an image into a Bitmap object, obtain a Graphics
for that bitmap, and use graphics primitives to write. When finished,
we’ll save the result to the page’s output stream and indicate a
particular MIME type.
The sample page that triggers the example is easily created, as shown in the following listing:
<html>
<body>
<img id="picture" src="dynimage.axd?url=images/pic1.jpg" />
</body>
</html>
The page contains no ASP.NET code and displays an image through a static HTML <img>
tag. The source of the image, though, is an HTTP handler that loads the
image passed through the query string, and then manipulates and
displays it. Here’s the source code for the ProcessRequest method of the HTTP handler:
public void ProcessRequest (HttpContext context)
{
object o = context.Request["url"];
if (o == null)
{
context.Response.Write("No image found.");
context.Response.End();
return;
}
string file = context.Server.MapPath((string)o);
string msg = ConfigurationManager.AppSettings["CopyrightNote"];
if (File.Exists(file))
{
Bitmap bmp = AddCopyright(file, msg);
context.Response.ContentType = "image/jpeg";
bmp.Save(context.Response.OutputStream, ImageFormat.Jpeg);
bmp.Dispose();
}
else
{
context.Response.Write("No image found.");
context.Response.End();
}
}
Note that the
server-side page performs two different tasks indeed. First, it writes
copyright text on the image canvas; next, it converts whatever the
original format was to JPEG:
Bitmap AddCopyright(string file, string msg)
{
// Load the file and create the graphics
Bitmap bmp = new Bitmap(file);
Graphics g = Graphics.FromImage(bmp);
// Define text alignment
StringFormat strFmt = new StringFormat();
strFmt.Alignment = StringAlignment.Center;
// Create brushes for the bottom writing
// (green text on black background)
SolidBrush btmForeColor = new SolidBrush(Color.PaleGreen);
SolidBrush btmBackColor = new SolidBrush(Color.Black);
// To calculate writing coordinates, obtain the size of the
// text given the font typeface and size
Font btmFont = new Font("Verdana", 7);
SizeF textSize = new SizeF();
textSize = g.MeasureString(msg, btmFont);
// Calculate the output rectangle and fill
float x = ((float) bmp.Width-textSize.Width-3);
float y = ((float) bmp.Height-textSize.Height-3);
float w = ((float) x + textSize.Width);
float h = ((float) y + textSize.Height);
RectangleF textArea = new RectangleF(x, y, w, h);
g.FillRectangle(btmBackColor, textArea);
// Draw the text and free resources
g.DrawString(msg, btmFont, btmForeColor, textArea);
btmForeColor.Dispose();
btmBackColor.Dispose();
btmFont.Dispose();
g.Dispose();
return bmp;
}
Figure 7 shows the results.
Note that the
additional text is part of the image the user downloads on her client
browser. If the user saves the picture by using the Save Picture As menu from the browser, the text (in this case, the copyright note) is saved along with the image.
Note
What
if the user requests the JPG file directly from the address bar? And
what if the image is linked by another Web site or referenced in a blog
post? In these cases, the original image is served without any further
modification. Why is it so? As mentioned, for performance reasons IIS
serves static files, such as JPG images, directly without involving any
external module, including the ASP.NET runtime. The HTTP handler that
does the trick of adding a copyright note is therefore blissfully
ignored when the request is made via the address bar or a hyperlink.
What can you do about it? In
IIS 6.0, you must register the JPG extension as an ASP.NET extension for
a particular application using the IIS Manager as shown in Figure 18-4. In this case, each request for JPG resources is forwarded to your application and resolved through the HTTP handler. In IIS 7.0, things are even simpler for developers. All that you have to do is add the following lines to the application’s web.config file: <system.webServer>
<handlers>
<add verb="*"
path="*.jpg"
type="Core35.Components.DynImageHandler,Core35Lib" />
</handlers>
</system.webServer>
The system.webServer section is a direct child of the root configuration node. |